package org.unidata.mdm.rest.data.service.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.DeleteRelationsRequestContext;
import org.unidata.mdm.data.context.MergeRelationsRequestContext;
import org.unidata.mdm.data.context.RestoreRelationsRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationsRequestContext;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.rest.data.converter.FullRecordConverter;
import org.unidata.mdm.rest.data.converter.IntegralRecordEtalonConverter;
import org.unidata.mdm.rest.data.converter.RelationToEtalonConverter;
import org.unidata.mdm.rest.data.ro.EtalonIntegralRecordRO;
import org.unidata.mdm.rest.data.ro.EtalonRelationToRO;
import org.unidata.mdm.rest.data.ro.FullRecordRO;
import org.unidata.mdm.rest.data.ro.RelationContainsWrapperRO;
import org.unidata.mdm.rest.data.ro.RelationManyToManyWrapperRO;
import org.unidata.mdm.rest.data.ro.RelationReferencesWrapperRO;
import org.unidata.mdm.rest.data.type.rendering.DataRestInputRenderingAction;
import org.unidata.mdm.rest.data.type.rendering.DataRestOutputRenderingAction;
import org.unidata.mdm.system.type.pipeline.fragment.InputFragmentCollector;
import org.unidata.mdm.system.type.pipeline.fragment.OutputFragmentContainer;
import org.unidata.mdm.system.type.rendering.InputFragmentRenderer;
import org.unidata.mdm.system.type.rendering.InputRenderingAction;
import org.unidata.mdm.system.type.rendering.MapInputSource;
import org.unidata.mdm.system.type.rendering.OutputFragmentRenderer;
import org.unidata.mdm.system.type.rendering.OutputRenderingAction;
import org.unidata.mdm.system.type.rendering.RenderingProvider;
import org.unidata.mdm.system.util.ConvertUtils;
import org.unidata.mdm.system.util.IdUtils;
import org.unidata.mdm.system.util.JsonUtils;

/**
 * @author Mikhail Mikhailov on Jan 16, 2020
 */
@Component
public class DataRenderingProvider implements RenderingProvider {

    private static final String RELATIONS_REFERENCE_PROPERTY_NAME = "relationReference";

    private static final String RELATIONS_CONTAINS_PROPERTY_NAME = "relationContains";

    private static final String RELATIONS_M2M_PROPERTY_NAME = "relationManyToMany";

    @Autowired
    private MetaModelService metaModelService;

    @Override
    public Collection<InputFragmentRenderer> get(InputRenderingAction action) {

        if (DataRestInputRenderingAction.ATOMIC_UPSERT_INPUT == action) {
//            return Collections.singletonList((c, s) -> renderAtomicUpsertInput((InputFragmentCollector<?>) c, (FullRecordRO) s));
            return Collections.singletonList(new AtomicUpsertInputRenderer(metaModelService));
        } else if (DataRestInputRenderingAction.MERGE_INPUT == action) {
            return Collections.singletonList((v, c, s) -> renderMergeInput((InputFragmentCollector<?>) c));
        } else if (DataRestInputRenderingAction.RECORD_RESTORE_INPUT == action) {
            return Collections.singletonList((v, c, s) -> renderRestoreRecordInput((InputFragmentCollector<?>) c));
        } else if (DataRestInputRenderingAction.PERIOD_RESTORE_INPUT == action) {
            return Collections.singletonList((v, c, s) -> renderRestorePeriodInput((InputFragmentCollector<?>) c, (MapInputSource) s));
        }

        return Collections.emptyList();
    }

    @Override
    public Collection<OutputFragmentRenderer> get(OutputRenderingAction action) {

        if (DataRestOutputRenderingAction.ATOMIC_UPSERT_OUTPUT == action) {
            return Collections.singletonList((v, c, s) -> renderAtomicUpsertOutput((OutputFragmentContainer) c, (FullRecordRO) s));
        }

        return Collections.emptyList();
    }

    private void renderAtomicUpsertOutput(OutputFragmentContainer container, FullRecordRO output) {
        // Nothing so far. Added for symmetry.
    }

    /*
     * Render 'classifier' part of the atomic upsert.
     */
    @Deprecated
    private void renderAtomicUpsertInput(InputFragmentCollector<?> collector, FullRecordRO input) {

        // Upserts collection
        Map<String, List<UpsertRelationRequestContext>> upserts = new HashMap<>();
        Map<String, List<DeleteRelationRequestContext>> deletes = new HashMap<>();

        // Containments
        JsonNode node = input.getAny().get(RELATIONS_CONTAINS_PROPERTY_NAME);
        RelationContainsWrapperRO contains = Objects.isNull(node) || node.isNull()
                ? null
                : JsonUtils.read(node, RelationContainsWrapperRO.class);

        // Refs
        node = input.getAny().get(RELATIONS_REFERENCE_PROPERTY_NAME);
        RelationReferencesWrapperRO reference = Objects.isNull(node) || node.isNull()
                ? null
                : JsonUtils.read(node, RelationReferencesWrapperRO.class);

        // M2Ms
        node = input.getAny().get(RELATIONS_M2M_PROPERTY_NAME);
        RelationManyToManyWrapperRO manyToMany = Objects.isNull(node) || node.isNull()
                ? null
                : JsonUtils.read(node, RelationManyToManyWrapperRO.class);

        // Process containments
        if (Objects.nonNull(contains)) {
            // Upsert
            if (CollectionUtils.isNotEmpty(contains.getToUpdate())) {

                for (EtalonIntegralRecordRO ro : contains.getToUpdate()) {

                    String toEtalonId = ro.getEtalonRecord() != null ? ro.getEtalonRecord().getEtalonId() : null;
                    String toSourceSystem = toEtalonId == null ? metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName() : null;
                    String toExternalId = toEtalonId == null ? IdUtils.v1String() : null;
                    String toEntityName = toEtalonId == null ? metaModelService.instance(Descriptors.DATA).getRelation(ro.getRelName()).getRight().getName() : null;
                    Date validFrom = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidFrom()) : null;
                    Date validTo = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidTo()) : null;
                    DataRecord converted = IntegralRecordEtalonConverter.from(ro);

                    upserts.computeIfAbsent(ro.getRelName(), k -> new ArrayList<UpsertRelationRequestContext>())
                           .add(UpsertRelationRequestContext.builder()
                                .relationEtalonKey(ro.getEtalonId())
                                .etalonKey(toEtalonId)
                                .sourceSystem(toSourceSystem)
                                .externalId(toExternalId)
                                .entityName(toEntityName)
                                .record(converted)
                                .relationName(ro.getRelName())
                                .validFrom(validFrom)
                                .validTo(validTo)
                                .build());
                }
            }
            // Delete
            if (CollectionUtils.isNotEmpty(contains.getToDelete())) {
                contains.getToDelete().forEach(dro ->
                    deletes.computeIfAbsent(dro.getRelName(), kg -> new ArrayList<DeleteRelationRequestContext>())
                           .add(FullRecordConverter.convert(dro, input)));
            }
        }

        // Process refs
        if (Objects.nonNull(reference)) {
            // Upsert
            if (CollectionUtils.isNotEmpty(reference.getToUpdate())) {

                for (EtalonRelationToRO ro : reference.getToUpdate()) {

                    DataRecord converted = RelationToEtalonConverter.from(ro);
                    upserts.computeIfAbsent(ro.getRelName(), k -> new ArrayList<UpsertRelationRequestContext>())
                           .add(UpsertRelationRequestContext.builder()
                                .etalonKey(ro.getEtalonIdTo())
                                .record(converted)
                                .relationName(ro.getRelName())
                                .validFrom(ConvertUtils.localDateTime2Date(ro.getValidFrom()))
                                .validTo(ConvertUtils.localDateTime2Date(ro.getValidTo()))
                                .sourceSystem(metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName())
                                .build());
                }
            }
            // Delete
            if (CollectionUtils.isNotEmpty(reference.getToDelete())) {
                reference.getToDelete().forEach(dro ->
                    deletes.computeIfAbsent(dro.getRelName(), kg -> new ArrayList<DeleteRelationRequestContext>())
                           .add(FullRecordConverter.convert(dro, input)));
            }
        }

        // Process M2Ms
        if (Objects.nonNull(manyToMany)) {
            // Upsert
            if (CollectionUtils.isNotEmpty(manyToMany.getToUpdate())) {

                for (EtalonRelationToRO ro : manyToMany.getToUpdate()) {
                    DataRecord converted = RelationToEtalonConverter.from(ro);
                    upserts.computeIfAbsent(ro.getRelName(), k -> new ArrayList<UpsertRelationRequestContext>())
                           .add((UpsertRelationRequestContext.builder()
                                .etalonKey(ro.getEtalonIdTo())
                                .record(converted)
                                .relationName(ro.getRelName())
                                .validFrom(ConvertUtils.localDateTime2Date(ro.getValidFrom()))
                                .validTo(ConvertUtils.localDateTime2Date(ro.getValidTo()))
                                .sourceSystem(metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName())
                                .build()));
                }
            }
            // Delete
            if (CollectionUtils.isNotEmpty(manyToMany.getToDelete())) {
                manyToMany.getToDelete().forEach(dro ->
                    deletes.computeIfAbsent(dro.getRelName(), kg -> new ArrayList<DeleteRelationRequestContext>())
                           .add(FullRecordConverter.convert(dro, input)));
            }
        }

        // Add relations upsert
        if (MapUtils.isNotEmpty(upserts)) {
            collector.fragment(UpsertRelationsRequestContext.builder()
                    .relationsFrom(upserts)
                    .build());
        }

        if (MapUtils.isNotEmpty(deletes)) {
            collector.fragment(DeleteRelationsRequestContext.builder()
                    .relationsFrom(deletes)
                    .build());
        }
    }

    private void renderMergeInput(InputFragmentCollector<?> collector) {
        collector.fragment(MergeRelationsRequestContext.builder()
                .applyToAll(true)
                .build());
    }

    private void renderRestorePeriodInput(InputFragmentCollector<?> collector, MapInputSource mis) {
        collector.fragment(
            RestoreRelationsRequestContext.builder()
                .applyToAll(true)
                .periodRestore(true)
                .validFrom(mis.getDate("validFrom"))
                .validTo(mis.getDate("validTo"))
                .build());
    }

    private void renderRestoreRecordInput(InputFragmentCollector<?> collector) {
        collector.fragment(
            RestoreRelationsRequestContext.builder()
                .applyToAll(true)
                .build());
    }
}
